package com.bugsnag.ohos.internal;

import ohos.app.Context;
import ohos.bundle.ApplicationInfo;
import ohos.bundle.BundleInfo;
import ohos.bundle.IBundleManager;
import ohos.rpc.RemoteException;

import com.bugsnag.ohos.BreadcrumbType;
import com.bugsnag.ohos.Configuration;
import com.bugsnag.ohos.Connectivity;
import com.bugsnag.ohos.DebugLogger;
import com.bugsnag.ohos.DefaultDelivery;
import com.bugsnag.ohos.Delivery;
import com.bugsnag.ohos.DeliveryHeaders;
import com.bugsnag.ohos.DeliveryParams;
import com.bugsnag.ohos.EndpointConfiguration;
import com.bugsnag.ohos.ErrorTypes;
import com.bugsnag.ohos.EventPayload;
import com.bugsnag.ohos.Logger;
import com.bugsnag.ohos.ManifestConfigLoader;
import com.bugsnag.ohos.NoopLogger;
import com.bugsnag.ohos.ThreadSendPolicy;
import com.bugsnag.ohos.ThrowableUtils;
import com.bugsnag.ohos.annotation.VisibleForTesting;

import java.io.File;
import java.util.Collection;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

/**
 * ImmutableConfig
 *
 * @since 2021-07-01
 */
public class ImmutableConfig {
    private static final String RELEASE_STAGE_DEVELOPMENT = "development";
    private static final String RELEASE_STAGE_PRODUCTION = "production";
    private String apiKey;
    private boolean autoDetectErrors;
    private ErrorTypes enabledErrorTypes;
    private boolean autoTrackSessions;
    private ThreadSendPolicy sendThreads;
    private Set<String> discardClasses;
    private Collection<String> enabledReleaseStages;
    private Collection<String> projectPackages;
    private Set<BreadcrumbType> enabledBreadcrumbTypes;
    private String releaseStage;
    private String buildUuid;
    private String appVersion;
    private int versionCode;
    private String appType;
    private Delivery delivery;
    private EndpointConfiguration endpoints;
    private boolean persistUser;
    private long launchDurationMillis;
    private Logger logger;
    private int maxBreadcrumbs;
    private int maxPersistedEvents;
    private int maxPersistedSessions;
    private File persistenceDirectory;
    private boolean sendLaunchCrashesSynchronously;

    // results cached here to avoid unnecessary lookups in Client.
    private BundleInfo packageInfo;
    private ApplicationInfo appInfo;

    /**
     * constructor
     *
     * @param apiKey
     * @param autoDetectErrors
     * @param enabledErrorTypes
     * @param autoTrackSessions
     * @param sendThreads
     * @param discardClasses
     * @param enabledReleaseStages
     * @param projectPackages
     * @param enabledBreadcrumbTypes
     * @param releaseStage
     * @param buildUuid
     * @param appVersion
     * @param versionCode
     * @param appType
     * @param delivery
     * @param endpoints
     * @param persistUser
     * @param launchDurationMillis
     * @param logger
     * @param maxBreadcrumbs
     * @param maxPersistedEvents
     * @param maxPersistedSessions
     * @param persistenceDirectory
     * @param sendLaunchCrashesSynchronously
     * @param packageInfo
     * @param appInfo
     */
    public ImmutableConfig(String apiKey,
                           boolean autoDetectErrors,
                           ErrorTypes enabledErrorTypes,
                           boolean autoTrackSessions,
                           ThreadSendPolicy sendThreads,
                           Set<String> discardClasses,
                           Collection<String> enabledReleaseStages,
                           Collection<String> projectPackages,
                           Set<BreadcrumbType> enabledBreadcrumbTypes,
                           String releaseStage,
                           String buildUuid,
                           String appVersion,
                           int versionCode,
                           String appType,
                           Delivery delivery,
                           EndpointConfiguration endpoints,
                           boolean persistUser,
                           long launchDurationMillis,
                           Logger logger,
                           int maxBreadcrumbs,
                           int maxPersistedEvents,
                           int maxPersistedSessions,
                           File persistenceDirectory,
                           boolean sendLaunchCrashesSynchronously,
                           BundleInfo packageInfo,
                           ApplicationInfo appInfo) {
        this.apiKey = apiKey;
        this.autoDetectErrors = autoDetectErrors;
        this.enabledErrorTypes = enabledErrorTypes;
        this.autoTrackSessions = autoTrackSessions;
        this.sendThreads = sendThreads;
        this.discardClasses = discardClasses;
        this.enabledReleaseStages = enabledReleaseStages;
        this.projectPackages = projectPackages;
        this.enabledBreadcrumbTypes = enabledBreadcrumbTypes;
        this.releaseStage = releaseStage;
        this.buildUuid = buildUuid;
        this.appVersion = appVersion;
        this.versionCode = versionCode;
        this.appType = appType;
        this.delivery = delivery;
        this.endpoints = endpoints;
        this.persistUser = persistUser;
        this.launchDurationMillis = launchDurationMillis;
        this.logger = logger;
        this.maxBreadcrumbs = maxBreadcrumbs;
        this.maxPersistedEvents = maxPersistedEvents;
        this.maxPersistedSessions = maxPersistedSessions;
        this.persistenceDirectory = persistenceDirectory;
        this.sendLaunchCrashesSynchronously = sendLaunchCrashesSynchronously;
        this.packageInfo = packageInfo;
        this.appInfo = appInfo;
    }

    public String getApiKey() {
        return apiKey;
    }

    public void setApiKey(String apiKey) {
        this.apiKey = apiKey;
    }

    public boolean getAutoDetectErrors() {
        return autoDetectErrors;
    }

    public void setAutoDetectErrors(boolean autoDetectErrors) {
        this.autoDetectErrors = autoDetectErrors;
    }

    public ErrorTypes getEnabledErrorTypes() {
        return enabledErrorTypes;
    }

    public void setEnabledErrorTypes(ErrorTypes enabledErrorTypes) {
        this.enabledErrorTypes = enabledErrorTypes;
    }

    public boolean getAutoTrackSessions() {
        return autoTrackSessions;
    }

    public void setAutoTrackSessions(boolean autoTrackSessions) {
        this.autoTrackSessions = autoTrackSessions;
    }

    public ThreadSendPolicy getSendThreads() {
        return sendThreads;
    }

    public void setSendThreads(ThreadSendPolicy sendThreads) {
        this.sendThreads = sendThreads;
    }

    public Set<String> getDiscardClasses() {
        return discardClasses;
    }

    public void setDiscardClasses(Set<String> discardClasses) {
        this.discardClasses = discardClasses;
    }

    public Collection<String> getEnabledReleaseStages() {
        return enabledReleaseStages;
    }

    public void setEnabledReleaseStages(Collection<String> enabledReleaseStages) {
        this.enabledReleaseStages = enabledReleaseStages;
    }

    public Collection<String> getProjectPackages() {
        return projectPackages;
    }

    public void setProjectPackages(Collection<String> projectPackages) {
        this.projectPackages = projectPackages;
    }

    public Set<BreadcrumbType> getEnabledBreadcrumbTypes() {
        return enabledBreadcrumbTypes;
    }

    public void setEnabledBreadcrumbTypes(Set<BreadcrumbType> enabledBreadcrumbTypes) {
        this.enabledBreadcrumbTypes = enabledBreadcrumbTypes;
    }

    public String getReleaseStage() {
        return releaseStage;
    }

    public void setReleaseStage(String releaseStage) {
        this.releaseStage = releaseStage;
    }

    public String getBuildUuid() {
        return buildUuid;
    }

    public void setBuildUuid(String buildUuid) {
        this.buildUuid = buildUuid;
    }

    public String getAppVersion() {
        return appVersion;
    }

    public void setAppVersion(String appVersion) {
        this.appVersion = appVersion;
    }

    public int getVersionCode() {
        return versionCode;
    }

    public void setVersionCode(int versionCode) {
        this.versionCode = versionCode;
    }

    public String getAppType() {
        return appType;
    }

    public void setAppType(String appType) {
        this.appType = appType;
    }

    public Delivery getDelivery() {
        return delivery;
    }

    public void setDelivery(Delivery delivery) {
        this.delivery = delivery;
    }

    public EndpointConfiguration getEndpoints() {
        return endpoints;
    }

    public void setEndpoints(EndpointConfiguration endpoints) {
        this.endpoints = endpoints;
    }

    public boolean getPersistUser() {
        return persistUser;
    }

    public void setPersistUser(boolean persistUser) {
        this.persistUser = persistUser;
    }

    public long getLaunchDurationMillis() {
        return launchDurationMillis;
    }

    public void setLaunchDurationMillis(long launchDurationMillis) {
        this.launchDurationMillis = launchDurationMillis;
    }

    public Logger getLogger() {
        return logger;
    }

    public void setLogger(Logger logger) {
        this.logger = logger;
    }

    public int getMaxBreadcrumbs() {
        return maxBreadcrumbs;
    }

    public void setMaxBreadcrumbs(int maxBreadcrumbs) {
        this.maxBreadcrumbs = maxBreadcrumbs;
    }

    public int getMaxPersistedEvents() {
        return maxPersistedEvents;
    }

    public void setMaxPersistedEvents(int maxPersistedEvents) {
        this.maxPersistedEvents = maxPersistedEvents;
    }

    public int getMaxPersistedSessions() {
        return maxPersistedSessions;
    }

    public void setMaxPersistedSessions(int maxPersistedSessions) {
        this.maxPersistedSessions = maxPersistedSessions;
    }

    public File getPersistenceDirectory() {
        return persistenceDirectory;
    }

    public void setPersistenceDirectory(File persistenceDirectory) {
        this.persistenceDirectory = persistenceDirectory;
    }

    public boolean getSendLaunchCrashesSynchronously() {
        return sendLaunchCrashesSynchronously;
    }

    public void setSendLaunchCrashesSynchronously(boolean sendLaunchCrashesSynchronously) {
        this.sendLaunchCrashesSynchronously = sendLaunchCrashesSynchronously;
    }

    public BundleInfo getPackageInfo() {
        return packageInfo;
    }

    public void setPackageInfo(BundleInfo packageInfo) {
        this.packageInfo = packageInfo;
    }

    public ApplicationInfo getAppInfo() {
        return appInfo;
    }

    public void setAppInfo(ApplicationInfo appInfo) {
        this.appInfo = appInfo;
    }

    /**
     * getErrorApiDeliveryParams
     *
     * @param payload
     * @return DeliveryParams
     */
    public DeliveryParams getErrorApiDeliveryParams(EventPayload payload) {
        Map<String, String> map = DeliveryHeaders.errorApiHeaders(payload);
        return new DeliveryParams(endpoints.getNotify(), map);
    }

    /**
     * getSessionApiDeliveryParams
     *
     * @return DeliveryParams
     */
    public DeliveryParams getSessionApiDeliveryParams() {
        return new DeliveryParams(endpoints.getSessions(), DeliveryHeaders.sessionApiHeaders(apiKey));
    }

    /**
     * Returns whether the given throwable should be discarded
     * based on the automatic data capture settings in [Configuration].
     *
     * @param exc
     * @return boolean
     */
    public boolean shouldDiscardError(Throwable exc) {
        return shouldDiscardByReleaseStage() || shouldDiscardByErrorClass(exc);
    }

    /**
     * Returns whether the given error should be discarded
     * based on the automatic data capture settings in [Configuration].
     *
     * @param errorClass
     * @return boolean
     */
    public boolean shouldDiscardError(String errorClass) {
        return shouldDiscardByReleaseStage() || shouldDiscardByErrorClass(errorClass);
    }

    /**
     * Returns whether a session should be discarded based on the
     * automatic data capture settings in [Configuration].
     *
     * @param autoCaptured
     * @return boolean
     */
    public boolean shouldDiscardSession(boolean autoCaptured) {
        return shouldDiscardByReleaseStage() || (autoCaptured && !autoTrackSessions);
    }

    /**
     * Returns whether breadcrumbs with the given type should be discarded or not.
     *
     * @param type
     * @return boolean
     */
    public boolean shouldDiscardBreadcrumb(BreadcrumbType type) {
        return enabledBreadcrumbTypes != null && !enabledBreadcrumbTypes.contains(type);
    }

    /**
     * Returns whether errors/sessions should be discarded or not based on the enabled
     * release stages.
     *
     * @return boolean
     */
    public boolean shouldDiscardByReleaseStage() {
        return enabledReleaseStages != null && !enabledReleaseStages.contains(releaseStage);
    }

    /**
     * Returns whether errors with the given errorClass should be discarded or not.
     *
     * @param errorClass
     * @return boolean
     */
    @VisibleForTesting
    public boolean shouldDiscardByErrorClass(String errorClass) {
        return discardClasses.contains(errorClass);
    }

    /**
     * Returns whether errors should be discarded or not based on the errorClass, as deduced
     * by the Throwable's class name.
     *
     * @param exc
     * @return boolean
     */
    @VisibleForTesting
    public boolean shouldDiscardByErrorClass(Throwable exc) {
        List<Throwable> throwables = ThrowableUtils.safeUnrollCauses(exc);
        for (Throwable throwable : throwables) {
            if (shouldDiscardByErrorClass(throwable.getClass().getName())) {
                return true;
            }
        }
        return false;
    }

    /**
     * convert To ImmutableConfig
     *
     * @param config
     * @param buildUuid
     * @param bundleInfo
     * @param appInfo
     * @return ImmutableConfig
     */
    public static ImmutableConfig convertToImmutableConfig(Configuration config, String buildUuid,
                                                           BundleInfo bundleInfo, ApplicationInfo appInfo) {
        ErrorTypes errorTypes;
        if (config.getAutoDetectErrors()) {
            errorTypes = config.getEnabledErrorTypes().copy();
        } else {
            errorTypes = new ErrorTypes(true);
        }

        return new ImmutableConfig(
            config.getApiKey(),
            config.getAutoDetectErrors(),
            errorTypes,
            config.getAutoTrackSessions(),
            config.getSendThreads(),
            config.getDiscardClasses(),
            config.getEnabledReleaseStages(),
            config.getProjectPackages(),
            config.getEnabledBreadcrumbTypes(),
            config.getReleaseStage(),
            buildUuid,
            config.getAppVersion(),
            config.getVersionCode(),
            config.getAppType(),
            config.getDelivery(),
            config.getEndpoints(),
            config.getPersistUser(),
            config.getLaunchDurationMillis(),
            config.getLogger(),
            config.getMaxBreadcrumbs(),
            config.getMaxPersistedEvents(),
            config.getMaxPersistedSessions(),
            config.getPersistenceDirectory(),
            config.getSendLaunchCrashesSynchronously(),
            bundleInfo,
            appInfo
        );
    }

    /**
     * sanitise Configuration
     *
     * @param appContext
     * @param configuration
     * @param connectivity
     * @return ImmutableConfig
     */
    public static ImmutableConfig sanitiseConfiguration(
        Context appContext, Configuration configuration, Connectivity connectivity) {
        String packageName = appContext.getBundleName();
        IBundleManager bundleManager = appContext.getBundleManager();
        BundleInfo bundleInfo = null;
        try {
            bundleInfo = bundleManager.getBundleInfo(packageName, IBundleManager.GET_BUNDLE_DEFAULT);
        } catch (RemoteException e) {
            e.getMessage();
        }

        ApplicationInfo appInfo = null;
        try {
            appInfo = bundleManager.getApplicationInfo(packageName, IBundleManager.GET_BUNDLE_DEFAULT, 0);
        } catch (RemoteException e) {
            e.getMessage();
        }

        if (configuration.getReleaseStage() == null) {
            if (appInfo != null && appInfo.debug) {
                configuration.setReleaseStage(RELEASE_STAGE_DEVELOPMENT);
            } else {
                configuration.setReleaseStage(RELEASE_STAGE_PRODUCTION);
            }
        }

        if (configuration.getLogger() == null
            || configuration.getLogger() == DebugLogger.INSTANCE) {
            String releaseStage = configuration.getReleaseStage();
            boolean loggingEnabled = releaseStage != RELEASE_STAGE_PRODUCTION;
            if (loggingEnabled) {
                configuration.setLogger(DebugLogger.INSTANCE);
            } else {
                configuration.setLogger(NoopLogger.INSTANCE);
            }
        }

        if (configuration.getVersionCode() == 0) {
            if (bundleInfo != null) {
                configuration.setVersionCode(bundleInfo.getVersionCode());
            }
        }

        if (configuration.getProjectPackages().isEmpty()) {
            Set<String> packages = new HashSet<>();
            packages.add(packageName);
            configuration.setProjectPackages(packages);
        }

        String buildUuid = ManifestConfigLoader.getString(ManifestConfigLoader.BUILD_UUID);
        if (configuration.getDelivery() == null) {
            configuration.setDelivery(new DefaultDelivery(connectivity, configuration.getLogger()));
        }

        if (configuration.getPersistenceDirectory() == null) {
            configuration.setPersistenceDirectory(appContext.getCacheDir());
        }
        return convertToImmutableConfig(configuration, buildUuid, bundleInfo, appInfo);
    }
}